﻿#region Usings

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;

using Gonte.Reflection; 

#endregion

namespace Gonte.Xml.Serialization
{
    /// <summary>
    /// Populates an object with xml data
    /// </summary>
    public class XmlObjectDeserializer
        : XmlTraversal
    {
        #region Constants

        const string TextProperty = "Text"; // For a text node there must exist a text propertyInfo on the object 

        #endregion

        #region Constructors

        protected XmlObjectDeserializer(Object @object)
        {
            object_ = @object;
        } 

        #endregion

        #region Methods

        /// <summary>
        /// Factory method to get a deserializer for an object
        /// </summary>
        /// <param name="object">The object to get a deserializer from</param>
        /// <returns></returns>
        public static XmlObjectDeserializer GetDeserializer(Object @object)
        {
            ExceptionThrower.ThrowWhenNull("Object", @object);

            if (@object is IXmlSerializableObject)
            {
                IXmlSerializableObject serializableObject = @object as IXmlSerializableObject;
                return serializableObject.GetDeserializer();
            }

            if (PrimitiveType.IsPrimitive(@object)
                || @object.GetType().IsEnum)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlPrimitiveDeserializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlPrimitiveDeserializer(@object);
            }

            if (@object.GetType().IsArray)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlArrayDeserializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlArrayDeserializer(@object);
            }

            if (@object is IList)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlListDeserializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlListDeserializer(@object);
            }

            if (@object is IDictionary)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlDictionaryDeserializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlDictionaryDeserializer(@object);
            }

            if (@object is ICollection)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlCollectionDeserializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlCollectionDeserializer(@object);
            }

            Debug.WriteLine(
                    string.Format(
                        "Creating a XmlObjectDeserializer for object of type: {0}",
                        @object.GetType().FullName));

            return new XmlObjectDeserializer(@object);
        }

        /// <summary>
        /// Deserializes an object from a uri
        /// </summary>
        /// <param name="uri">The uri to retrieve the xml from</param>
        public void Deserialize(string uri)
        {
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.IgnoreComments = true;
            settings.IgnoreWhitespace = true;

            using (XmlReader xmlReader = XmlReader.Create(uri, settings))
            {
                Reader = xmlReader;
                Traverse();
            }
        }

        public Object Deserialize(Stream stream)
        {
            using (XmlReader reader = XmlReader.Create(stream))
            {
                Reader = reader;
                Traverse();
            }

            return object_;
        }

        /// <summary>
        /// Deserializes an object from an open xml reader
        /// </summary>
        /// <param name="reader">The xml reader</param>
        /// <param name="context"></param>
        /// <returns>The deserialized object</returns>
        public Object Deserialize(XmlReader reader, XmlSerializationContext context)
        {
            Reader = reader;
            context_ = context;
            Traverse();

            return object_;
        }

        /// <summary>
        /// Deserializes an object from an xml string
        /// </summary>
        /// <param name="xml">The xml string</param>
        public Object DeserializeFromString(string xml)
        {
            using (StringReader stringReader = new StringReader(xml))
            {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.IgnoreComments = true;
                settings.IgnoreWhitespace = true;

                using (XmlReader xmlReader = XmlReader.Create(stringReader, settings))
                {
                    Reader = xmlReader;
                    Traverse();
                }
            }

            return object_;
        }

        #endregion

        #region Overridables

        protected override bool OnBeforeReading()
        {
            Debug.WriteLine(
                string.Format(" ---> Entering the deserializer for type: {0}",
                object_.GetType().FullName
                ));

            if (Reader.IsStartElement()) // The element was already read by the caller deserializer
            {
                Debug.WriteLine("The element was already read by the caller deserializer");

                if (!ProcessElement())
                {
                    return false;
                }
            }

            return true; // Continue reading
        }

        protected override bool OnElement()
        {
            base.OnElement();

            string elementName = Reader.LocalName;

            if (string.IsNullOrEmpty(rootElementName_)) // Root element
            {
                VerifyElementNameMatchesObjectType(elementName);
                rootElementName_ = elementName;
            }
            else
            {
                // Get the property name
                Type type = object_.GetType();
                TypeAccessor typeAccessor = TypeAccessor.GetTypeAccessor(type);
                PropertyInfo propertyInfo = FindProperty(type, elementName);
                string propertyName = propertyInfo.Name;

                // Skip deserialization if the xml ignore attribute is set
                if (Attribute.IsDefined(propertyInfo, typeof(XmlIgnoreAttribute)))
                {
                    return false; // Do not deserialize the object
                }

                // Retrieve or create the child object
                Object child = typeAccessor.GetValue(object_, propertyName);

                if (child == null) // Create it
                {
                    Type childType = null;

                    if (null != Reader["xsi:type"])
                    {
                        childType = Type.GetType(Reader["xsi:type"]);
                        child = ObjectFactory.CreateObject(childType);
                    }
                    else
                    {
                        child = ObjectFactory.CreateObject(propertyInfo);
                    }

                    Debug.WriteLine(string.Format("Created current object object of type: {0}", child.GetType().FullName));
                } 

                // Get the deserializer
                XmlObjectDeserializer deserializer = XmlObjectDeserializer.GetDeserializer(child);

                // Create and populate the context
                XmlSerializationContext context = new XmlSerializationContext();
                context.Parent = object_;
                context.PropertyName = propertyName;
                context.Attributes = CustomAttribute.GetCustomAttributes(propertyInfo, null, false);

                // Deserialize the child object
                child = deserializer.Deserialize(Reader, context);

                // Set the value back
                typeAccessor.SetValue(object_, propertyName, child);

                if (Reader.IsEmptyElement)
                {
                    return false; // Do not process attributes
                }
            }

            return true;
        }

        protected override bool OnEndElement()
        {
            base.OnEndElement();

            if (Reader.LocalName.Equals(rootElementName_)) // End of the root element name
            {
                OnAfterReading();
                return false; // Done
            }

            return true; // Read the next element
        }

        /// <summary>
        /// Called before exiting the current serializer
        /// </summary>
        protected virtual void OnAfterReading()
        {
            Debug.WriteLine(
                string.Format(" <--- Exiting the deserializer for type: {0}",
                object_.GetType().FullName
                ));
        }

        protected override void OnAttribute()
        {
            string attributeName = Reader.Name;

            switch (attributeName)
            {
                case "xmlns:xsi":
                    {
                        return; // Ignore the namespace
                    }
                case "xsi:type":
                    {
                        return; // Ignore the type of this object
                    }
                default:
                    {
                        Type type = object_.GetType();
                        PropertyInfo propertyInfo = FindProperty(type, Reader.Name);
                        string propertyName = propertyInfo.Name;

                        // Check if the xml ignore attribute is set for this property
                        if (Attribute.IsDefined(propertyInfo, typeof(XmlIgnoreAttribute)))
                        {
                            return; // Do not set the value
                        }

                        // Set the value
                        TypeAccessor.GetTypeAccessor(type).SetValue(
                            object_, propertyName, 
                            ObjectTypeConverter.ConvertValue(Reader.Value, propertyInfo.PropertyType)
                            );
                    }
                    break;
            }
        }
        
        protected override bool OnText()
        {
            string text = Reader.Value.Trim();

            TypeAccessor typeAccessor = TypeAccessor.GetTypeAccessor(object_.GetType());
            typeAccessor.SetValue(object_, TextProperty, text);

            return true; // Continue reading
        }

        #endregion

        #region Helpers

        protected void VerifyElementNameMatchesObjectType(string elementName)
        {
            Type type = object_.GetType();
            if (!elementName.Equals(type.Name))
            {
                if (context_ == null
                    || context_.Parent == null) // Root
                {
                    XmlRootAttribute xmlRootAttribute = CustomAttribute.GetCustomAttribute<XmlRootAttribute>(type, false);
                    if (xmlRootAttribute == null
                        || !elementName.Equals(xmlRootAttribute.ElementName))
                    {
                        throw new InvalidOperationException(
                        string.Format(
                            "The name of the node: {0} does not match the name of the type: {1}",
                            elementName,
                            type.Name
                            ));
                    }
                }
            }
        }

        /// <summary>
        /// Searches for a property name that matches the one in the node or the XmlElementAttribute if any
        /// </summary>
        /// <param name="elementName"></param>
        /// <returns></returns>
        protected PropertyInfo FindProperty(Type type, string elementName)
        {
            TypeAccessor typeAccessor = TypeAccessor.GetTypeAccessor(type);
            Debug.WriteLine(string.Format("Mapping node name: {0} for object: {1}", elementName, type.FullName));

            if (typeAccessor.Contains(elementName)) // The xml element has the same name as the object property
            {
                Debug.WriteLine(string.Format("The node with name: {0} was found in the properties.", elementName));
                
                return type.GetProperty(elementName);
            }

            // Find if there is a match between the xml element name and the custom (XmlElementAttribute) property name
            foreach (string property in typeAccessor.PropertyNames)
            {
                PropertyInfo propertyInfo = type.GetProperty(property);
                Debug.WriteLine(string.Format("Searching for XmlElementAttribute in property: {0}", property));

                Object[] attributes = CustomAttribute.GetCustomAttributes<XmlElementAttribute>(propertyInfo, false);
                if (attributes != null)
                {
                    foreach (XmlElementAttribute attribute in attributes)
                    {
                        if (attribute.ElementName.Equals(elementName))
                        {
                            Debug.WriteLine(
                                string.Format("The xml element name matches the one in the xml element attribute: {0}. Returning property name: {1}",
                                attribute.ElementName,
                                property));

                            return propertyInfo;
                        }
                    }
                }

                attributes = CustomAttribute.GetCustomAttributes<XmlAttributeAttribute>(propertyInfo, false);
                if (attributes != null)
                {
                    foreach (XmlAttributeAttribute attribute in attributes)
                    {
                        if (attribute.AttributeName.Equals(elementName))
                        {
                            Debug.WriteLine(
                                string.Format("The xml attribute name matches the one in the xml element attribute: {0}. Returning property name: {1}",
                                attribute.AttributeName,
                                property));

                            return propertyInfo;
                        }
                    }
                }
            }

            // None of the above operations worked, throwing an exception
            throw new ArgumentException(string.Format("Unable to find property: {0} for object: {1}", elementName, type.FullName));
        }

        #endregion

        #region Fields

        protected Object object_;
        protected string rootElementName_;
        protected XmlSerializationContext context_;

        #endregion
    }
}
